package org.jeecg.common.util;

import com.jcraft.jsch.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.jeecg.common.constant.SymbolConstant;
import org.jeecg.common.exception.JeecgBootException;
import org.jeecg.config.props.SftpConfig;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Vector;
import java.util.stream.Collectors;

/**
 * sftp工具类
 * @author: qiang.zhou
 * @date: 2024/9/5 9:48
 **/
@Slf4j
public class SftpUtil {

    public static final int ERROR_MSG_LENGTH = 255;

    private Session sftpSession;

    private SftpConfig.Config sftpConfig;

    private ChannelSftp channelSftp;

    /** sftp现网工作根目录 */
    private String WORK_ROOT_PATH = "/";

    private SftpUtil() { }

    public static SftpUtil getInstance(SftpConfig.Config config) {
        SftpUtil sftpUtil = new SftpUtil();
        String portStr = config.getPort();
        String[] ports = portStr.split(SymbolConstant.COMMA);
        boolean flag = false;

        for (String port: ports) {
            config.setPort(port);
            log.info("尝试连接SFTP; 地址: {}, 端口: {}", config.getHost(), config.getPort());
            flag = sftpUtil.connect(config, 0);
            if (flag) {
                // 设置的端口中只要有一个连接成功就不在尝试连接其他端口
                break;
            }
        }
        config.setPort(portStr);
        if (!flag) {
            // 如果设置的端口都失败，则抛出异常
            log.error("sftp无法连接, 使用的配置: {}", JacksonUtil.toJsonString(config));
            throw new JeecgBootException("无法连接文件服务器，请联系系统管理员");
        }
        return sftpUtil;
    }

    public boolean connect(SftpConfig.Config config, int retry) {
        if (config == null) {
            config = new SftpConfig.Config();
        }
        try {
            sftpConfig = config;
            log.info("SFTP Connect Start, retry: {}", retry);

            JSch jSch = new JSch();
            Session session = jSch.getSession(config.getUsername(), config.getHost(), Integer.parseInt(config.getPort()));
            session.setPassword(config.getPassword());
            Properties prop = new Properties();
            prop.put("StrictHostKeyChecking", "no");
            prop.putAll(config.getSessionConfig());
            // diffie-hellman-group1-sha1,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256
            session.setConfig("kex", "diffie-hellman-group1-sha1,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha1,diffie-hellman-group-exchange-sha256");
            session.setConfig(prop);
            session.connect();
            sftpSession = session;
            log.info("SFTP Session {} connected.", sftpConfig.getHost());

            Channel channel = session.openChannel("sftp");
            channel.connect();
            log.info("SFTP Channel {} Open Success.", sftpConfig.getHost());
            channelSftp = (ChannelSftp) channel;

            // 检查登录进来时的工作目录
            WORK_ROOT_PATH = channelSftp.pwd();
            // 如果sftp根目录
            if (!WORK_ROOT_PATH.endsWith(SymbolConstant.SINGLE_SLASH)) {
                WORK_ROOT_PATH += SymbolConstant.SINGLE_SLASH;
            }
            log.info("{}的sftp工作目录为: {}", config.getHost(), WORK_ROOT_PATH);
            return true;
        } catch (Exception e) {
            log.error("获取sftp连接失败, 原因: ", e);
            // 连接异常时重试
            if (retry < config.getRetry() && config.getRetry() >= 0) {
                retry = retry+1;
                return this.connect(config, retry);
            }
            return false;
        }
    }

    /**
     * 上传文件
     * @author: qiang.zhou
     * @date: 2023/7/12 11:23
     * @param directory 需要存储到sftp的文件夹,相对路径
     * @param sftpFileName 存储到sftp的文件名称
     * @param input 源文件流
     * @return void
     **/
    public void upload(String directory, String sftpFileName, InputStream input) {
        try {
            try {
                directory = getSftpPath(directory);
                channelSftp.cd(directory);
            } catch (SftpException e) {
                // 没有此目录需要创建
                this.mkdirDir(directory);
            }
            channelSftp.put(input, sftpFileName);
        } catch (SftpException e) {
            log.error("上传文件错误; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }

        try {
            input.close();
        } catch (Exception e) {
            log.warn("关闭流失败");
        }
    }

    /**
     * 断点续传的方式
     * @author: qiang.zhou
     * @date: 2024/9/5 10:31
     * @param directory
     * @param sftpFileName
     * @param fileSize
     * @param input
     * @return void
     **/
    public void uploadSlice(String directory, String sftpFileName, long fileSize, InputStream input) {
        try {
            try {
                directory = getSftpPath(directory);
                channelSftp.cd(directory);
            } catch (SftpException e) {
                // 没有此目录需要创建
                this.mkdirDir(directory);
            }
            // 检查并删除已存在文件
            try {
                channelSftp.rm(sftpFileName); // 删除现有文件
            } catch (SftpException e) {
                if (e.id != ChannelSftp.SSH_FX_NO_SUCH_FILE) {
                    throw e; // 非"文件不存在"异常则抛出
                }
            }
            // 获取当前文件上传的大小
            long current = this.fileSize(directory, sftpFileName);
            channelSftp.put(input, sftpFileName, new SftpProgressMonitor() {
                // 当前接收的总字节数
                private long count = 0;
                // 最终文件大小
                private long max = fileSize;
                // 进度
                private long percent = -1;

                @Override
                public void init(int op, String src, String dest, long max) {
                    log.info("Starting transfer: " + src + " --> " + dest);
                    this.count = current;
                }

                @Override
                public boolean count(long bytes) {
                    this.count += bytes;
                    long temp = this.count * 100 / max;
                    if (percent >= temp) {
                        return true;
                    }
                    percent = temp;

                    log.info("Transferred {} ({}%) out of {}", this.count, this.percent, this.max);
                    return true;
                }

                @Override
                public void end() {
                    log.info("Transfer complete.");
                }
            }, ChannelSftp.RESUME);
        } catch (SftpException e) {
            log.error("上传文件错误; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }

        try {
            input.close();
        } catch (Exception e) {
            log.warn("关闭流失败");
        }
    }

    /**
     * 下载文件
     * @author: qiang.zhou
     * @date: 2023/7/12 14:02
     * @param directory
     * @param sftpFileName
     * @return java.io.OutputStream
     **/
    public void download(String directory, String sftpFileName, String filePath) {
        try (FileOutputStream fos = new FileOutputStream(filePath)) {
            download(directory, sftpFileName, fos);
        } catch (IOException e) {
            log.error("写入文件失败; 原因: ", e);
            throw new JeecgBootException("写入文件失败; IO异常");
        }
    }

    /**
     * HttpServletResponse 获取sftp文件
     * @author: qiang.zhou
     * @date: 2023/7/14 11:27
     * @param directory
     * @param sftpFileName
     * @param response
     * @return void
     **/
    public void download(String directory, String sftpFileName, HttpServletResponse response) {
        try (BufferedInputStream bis = new BufferedInputStream(download(directory, sftpFileName));
             BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream())) {

            byte[] bytes = new byte[4096];
            int len;
            while ((len = bis.read(bytes)) != -1) {
                bos.write(bytes, 0, len);
            }
        } catch (IOException e) {
            log.error("网络异常; 原因: ", e);
            throw new JeecgBootException("下载文件错误; IO异常");
        }
    }

    public void download(String directory, String sftpFileName, OutputStream os) {
        try {
            channelSftp.cd(getSftpPath(directory));

            channelSftp.get(sftpFileName, os);
        } catch (SftpException e) {
            log.error("下载文件错误，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 获取InputStream流
     *  返回的InputStream流要及时关闭；如果流未关闭就执行其他sftp指令，则直接报错
     * @author: qiang.zhou
     * @date: 2024/9/25 16:54
     * @param directory
     * @param sftpFileName
     * @return java.io.InputStream
     **/
    public InputStream download(String directory, String sftpFileName) {
        try {
            channelSftp.cd(getSftpPath(directory));

            return channelSftp.get(sftpFileName);
        } catch (SftpException e) {
            log.error("下载文件错误，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 获取sftp文件夹中所有文件的最后修改时间
     * @author: qiang.zhou
     * @date: 2023/7/19 15:24
     * @param directory
     * @return java.util.List<java.lang.String>
     **/
    public List<String> fileTimeList(String directory) {
        try {
            channelSftp.cd(getSftpPath(directory));

            Vector<ChannelSftp.LsEntry> attrs = channelSftp.ls("*");
            return attrs.stream()
                    .map(entry -> entry.getAttrs().getMTime())
                    .map(mTime -> {
                        Date date = new Date(mTime * 1000L);
                        return DateUtils.formatDate(date);
                    }).collect(Collectors.toList());
        } catch (SftpException e) {
            log.error("下载文件错误，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 获取文件列表
     * @author: qiang.zhou
     * @date: 2023/7/19 14:16
     * @return void
     **/
    public String fileTime(String directory, String fileName) {
        try {
            channelSftp.cd(getSftpPath(directory));

            SftpATTRS attrs = channelSftp.lstat(fileName);
            Date date = new Date(attrs.getMTime() * 1000L);
            return DateUtils.formatDate(date);
        } catch (SftpException e) {
            log.error("获取文件列表失败，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 获取指定文件大小
     * @author: qiang.zhou
     * @date: 2024/4/22 16:07
     * @param directory
     * @param fileName
     * @return long
     **/
    public long fileSize(String directory, String fileName) {
        try {
            channelSftp.cd(getSftpPath(directory));

            SftpATTRS attrs = channelSftp.lstat(fileName);
            return attrs.getSize();
        } catch (SftpException e) {
            // 获取文件大小时，如没有当前文件直接返回0
            return 0L;
        }
    }

    /**
     * 获取文件夹下的文件名称
     * @author: qiang.zhou
     * @date: 2023/7/19 16:05
     * @param directory
     * @return java.util.List<java.lang.String>
     **/
    public List<String> fileNameList(String directory) {
        try {
            channelSftp.cd(getSftpPath(directory));

            Vector<ChannelSftp.LsEntry> attrs = channelSftp.ls("*");
            return attrs.stream()
                    .filter(entry -> !entry.getAttrs().isDir())
                    .map(entry -> entry.getFilename())
                    .collect(Collectors.toList());
        } catch (SftpException e) {
            log.error("下载文件错误，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 获取文件夹下的所有文件夹名称
     * @author: qiang.zhou
     * @date: 2023/7/24 18:14
     * @param directory
     * @return java.util.List<java.lang.String>
     **/
    public List<String> directoryNameList(String directory) {
        try {
            channelSftp.cd(getSftpPath(directory));

            Vector<ChannelSftp.LsEntry> attrs = channelSftp.ls("*");
            return attrs.stream()
                    .filter(entry -> entry.getAttrs().isDir())
                    .map(entry -> entry.getFilename())
                    .collect(Collectors.toList());
        } catch (SftpException e) {
            log.error("下载文件错误，没有哪个文件或目录; 原因: ", e);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    /**
     * 创建目录
     * @author: qiang.zhou
     * @date: 2023/7/24 16:53
     * @param directory
     * @return void
     **/
    public void mkdirDir(String directory) {
        String[] dirs = directory.split(SymbolConstant.SINGLE_SLASH);
        mkdirDir(dirs, "", dirs.length, 0);
    }

    /**
     * 递归创建文件路径
     * @author: qiang.zhou
     * @date: 2023/7/24 17:16
     * @param dirs
     * @param tempPath
     * @param length
     * @param index
     * @return void
     **/
    private void mkdirDir(String[] dirs, String tempPath, int length, int index) {
        index++;
        if (index < length) {
            tempPath += "/" + dirs[index];
        }

        try {
            channelSftp.cd(tempPath);
            if (index < length) {
                mkdirDir(dirs, tempPath, length, index);
            }
        } catch (SftpException e) {
            try {
                channelSftp.mkdir(tempPath);
                channelSftp.cd(tempPath);
            } catch (SftpException mkdirError) {
                log.error("{}路径创建失败; ", tempPath, mkdirError);
                throw new JeecgBootException(getErrorMessage(mkdirError, ERROR_MSG_LENGTH));
            }
            mkdirDir(dirs, tempPath, length, index);
        }

    }

    /**
     * 查看文件夹是否存在
     * @author: qiang.zhou
     * @date: 2023/7/27 15:42
     * @param directory
     * @return void
     **/
    public boolean directoryExist(String directory) {
        try {
            channelSftp.cd(getSftpPath(directory));
            return true;
        } catch (Exception e) {
            log.info("不存在的文件夹: {}", directory);
            return false;
        }

    }

    /**
     * 文件是否存在
     * @author: qiang.zhou
     * @date: 2023/7/27 15:44
     * @param directory
     * @param fileName
     * @return boolean true存在
     **/
    public boolean fileExist(String directory, String fileName) {
        try {
            channelSftp.cd(getSftpPath(directory));
            Vector<ChannelSftp.LsEntry> attrs = channelSftp.ls(fileName);
            if (null == attrs) {
                return false;
            }
            String name = attrs.stream().map(entry -> entry.getFilename()).findFirst().orElse(null);
            return StringUtils.isNotBlank(name);
        } catch (SftpException e) {
            log.error("不存在的文件: {}", getSftpPath(directory) + fileName);
            return false;
        }
    }

    /**
     * 修改文件名称
     * @author: qiang.zhou
     * @date: 2023/7/28 10:39
     * @param directory
     * @param oldName
     * @param newName
     * @return void
     **/
    public void rename(String directory, String oldName, String newName) {
        try {
            channelSftp.cd(getSftpPath(directory));
            channelSftp.rename(oldName, newName);
            log.info("修改文件夹{}中的文件名称, 由[{}]改为[{}]", directory, oldName, newName);
        } catch (SftpException e) {
            log.error("不存在的文件: {}/{}", getSftpPath(directory) , oldName);
            throw new JeecgBootException(getErrorMessage(e, ERROR_MSG_LENGTH), e);
        }
    }

    public void logout() {
        // 关闭信道
        if (null != channelSftp && channelSftp.isConnected()) {
            channelSftp.disconnect();
            log.info("Sftp Channel Is Close Already.");
        }

        if (null != sftpSession && sftpSession.isConnected()) {
            sftpSession.disconnect();
            log.info("Sftp Session Is Close Already.");
        }
    }

    public String getSftpPath(String directory) {
        String sftpPath = sftpConfig.getSftpRoot();
        // 判断sftp的根目录是否已经包含在配置的路径中
        if (!WORK_ROOT_PATH.equals(SymbolConstant.SINGLE_SLASH)
                && sftpPath.contains(WORK_ROOT_PATH)) {
            sftpPath = sftpPath.replace(WORK_ROOT_PATH, "");
        }

        if (sftpPath.startsWith(SymbolConstant.SINGLE_SLASH)) {
            sftpPath = sftpPath.substring(1);
        }

        sftpPath = WORK_ROOT_PATH + sftpPath;
        if (StringUtils.isBlank(directory)) {
            log.info("进入Sftp文件路径: {}", sftpPath);
            return sftpPath;
        }
        // 检查传入的路径是否和根路径是否有重叠
        String path;
        if (directory.startsWith(sftpPath)) {
            path = directory;
        } else {
            path = sftpPath + directory;
        }
        log.info("进入Sftp文件路径: {}", path);
        return path;
    }

    public String getErrorMessage(SftpException e, int maxLength) {
        String msg = e.getMessage();
        return msg.length() <= maxLength ? msg : msg.substring(0, maxLength);
    }

}
